#!/usr/bin/env python
# -*- coding: utf-8 -*-
####################################################################
# File: tv_tk.py
# Author: Jean-Marc VARENNE-PAQUET
# Description: This script is a graphical user interface (GUI), 
# that allows users to watch TV, by using the multiposte service 
# from the ADSL and fiber boxes.
# Version: 1.1
# License: GNU GPL v3
####################################################################



# Import
import os, sys, string, datetime, re, urllib2
import Tkinter, tkFileDialog, tkMessageBox, ttk, webbrowser


# Initialization
g_channel_name_list = []
g_channel_urls_hash_table = {}
g_channel_programs_hash_table = {}
g_favorite_channel_name_list = []
g_vlc = 'vlc'
g_configuration_directory = os.environ['HOME'] + "/.config/x_multipost_tv"
g_playlist_url_file_path = g_configuration_directory + "/playlist_url.txt"
g_favorites_file_path = g_configuration_directory + "/favorit_channels.txt"
g_tasks_file = g_configuration_directory + "/crontab_tasks.txt"
g_playlist_url = "http://mafreebox.freebox.fr/freeboxtv/playlist.m3u"










##########################################################
# Classes
##########################################################

# The main window
class Ui_MainWindow():
	def __init__(self, parent):
		self.parent = parent

		parent.title("X-Multiposte-TV")
		parent.geometry("450x750")



		#########################################################################################
		# Menu bar
		#########################################################################################

		# Create menu bar
		menu_bar = Tkinter.Menu(parent)
		parent.config(menu=menu_bar)

		# menu File
		menu_file = Tkinter.Menu(menu_bar, tearoff=False)
		menu_file.add_command(label="Ouvrir un enregistrement video... Ctrl+O", underline=0, command=self.openLocalVideo)
		menu_file.bind_all('<Control-o>', lambda event : self.openLocalVideo())
		menu_file.add_separator()
		menu_file.add_command(label="Quitter                           Ctrl+Q", underline=0, command=parent.quit)
		menu_file.bind_all('<Control-q>', lambda event : parent.quit())
		menu_bar.add_cascade(label="Fichier", underline=0, menu=menu_file)

		# menu TV guide
		menu_tv_guide = Tkinter.Menu(menu_bar, tearoff=False)
		menu_tv_guide.add_command(label="Ouvrir le guide TV", command= lambda: webbrowser.open_new("http://streamplayer.free.fr/tv/guidetv.php"))
		menu_bar.add_cascade(label="GuideTV", underline=0, menu=menu_tv_guide)

		# menu Configuration
		menu_configuration = Tkinter.Menu(menu_bar, tearoff=False)
		menu_configuration.add_command(label="Definir la playlist a utiliser", command=self.displayConfigurationWindow)
		menu_bar.add_cascade(label="Configuration", underline=0, menu=menu_configuration)

		# menu Help
		menu_help = Tkinter.Menu(menu_bar, tearoff=False)
		menu_help.add_command(label="A propos de cette application", command=self.displayAboutAppWindow)
		menu_bar.add_cascade(label="Aide", underline=0, menu=menu_help)




		#########################################################################################
		# Central Widget
		#########################################################################################

		notebook = ttk.Notebook(parent, name='notebook')
		notebook.pack(fill=Tkinter.BOTH, expand=Tkinter.Y, padx=2, pady=3)



		# Tab 1
		frame_tab_1 = ttk.Frame(notebook)

		# Enable automatic resizing
		frame_tab_1.grid_rowconfigure   (1, weight=1)
		frame_tab_1.grid_columnconfigure(0, weight=1)

		notebook.add(frame_tab_1, text="Toutes", underline=0, padding=2)

		# Filter field
		self.entry_search = ttk.Entry(frame_tab_1)
		self.entry_search.grid(column=0, row=0, columnspan=2, sticky='EW')
		self.entry_search.bind('<KeyRelease>', lambda event : self.updateModel())

		# Listbox
		self.list_box_page_1 = Tkinter.Listbox(frame_tab_1)
		self.list_box_page_1.grid(column=0, row=1, sticky='NSEW')
		self.list_box_page_1.bind("<Double-Button-1>", self.onListBox1DoubleClick)
		self.list_box_page_1.bind("<Button-3>", self.displayContextMenuAll)

		# Scrollbar
		scroll_bar_page_1 = ttk.Scrollbar(frame_tab_1)
		scroll_bar_page_1.grid(column=1, row=1, sticky='NS')
		
		# Link the Scrollbar and the Listbox together.
		scroll_bar_page_1.config(command = self.list_box_page_1.yview)
		self.list_box_page_1.config(yscrollcommand = scroll_bar_page_1.set)



		# Tab 2
		frame_tab_2 = ttk.Frame(notebook)

		# Enable automatic resizing
		frame_tab_2.grid_rowconfigure   (1, weight=1)
		frame_tab_2.grid_columnconfigure(0, weight=1)

		notebook.add(frame_tab_2, text="Favorites", underline=0, padding=2)

		# Listbox
		self.list_box_page_2 = Tkinter.Listbox(frame_tab_2)
		self.list_box_page_2.grid(column=0, row=1, sticky='NSEW')
		self.list_box_page_2.bind("<Double-Button-1>", self.onListBox2DoubleClick)
		self.list_box_page_2.bind("<Button-3>", self.displayContextMenuFavorites)

		# Scrollbar
		scroll_bar_page_2 = ttk.Scrollbar(frame_tab_2)
		scroll_bar_page_2.grid(column=1, row=1, sticky='NS')

		# Link the Scrollbar and the Listbox together.
		scroll_bar_page_2.config(command = self.list_box_page_2.yview)
		self.list_box_page_2.config(yscrollcommand = scroll_bar_page_2.set)



		# Set the focus on the filter field
		self.entry_search.focus_set()



		#########################################################################################
		# Create a directory for the configuration files
		#########################################################################################

		if not os.path.exists(g_configuration_directory):
			try:
				os.makedirs(g_configuration_directory)
			except OSError as msg:
				print(msg)



		#########################################################################################
		# Load data
		#########################################################################################

		self.readConfigurationFile()
		self.loadPlaylist()
		self.reloadFavoritesFile()

	# On double click on ListBox 1.
	def onListBox1DoubleClick(self, event):
		selection_index = self.list_box_page_1.curselection()[0]
		channel_name = self.list_box_page_1.get(selection_index)
		self.watchTV(channel_name)

	# On double click on ListBox 2.
	def onListBox2DoubleClick(self, event):
		selection_index = self.list_box_page_2.curselection()[0]
		channel_name = self.list_box_page_2.get(selection_index)
		self.watchTV(channel_name)

	# Update the channels list displayed. It takes into account the entry from the filter field. 
	def updateModel(self):
		string = self.entry_search.get()
		self.list_box_page_1.delete(0, Tkinter.END)
		for channel_name in g_channel_name_list:
			minus_channel_name = channel_name.lower()
			minus_string = string.lower()
			if minus_channel_name.find(minus_string) >= 0:
				self.list_box_page_1.insert(Tkinter.END, channel_name)

	# Run VLC to display a channel.
	def watchTV(self, channel_name):
		channel_url = g_channel_urls_hash_table[channel_name]
		command = g_vlc + " --video-filter=deinterlace --deinterlace-mode=yadif2x \"" + channel_url + "\" &"
		print(command)
		os.system(command)

	# Run VLC to watch and record a channel at the same time.
	def watchAndRecordTV(self, channel_name):
		channel_url = g_channel_urls_hash_table[channel_name]
		output_file_path = os.environ['HOME'] + os.sep + channel_name.encode('utf-8').replace(" ","") + "__" + str(datetime.datetime.today()).replace(":","-").replace(" ","__") + ".ts"
		command = g_vlc + " \"" + channel_url + "\" --sout=\"#duplicate{dst=std{access=file,mux=ts,dst=" + output_file_path + "},dst=display}\" &"
		print(command)
		os.system(command)

	# Display a window which allows to plan a record from a date to an other.
	def planRecord(self, channel_name):
		channel_url = g_channel_urls_hash_table[channel_name]
		top_level = Tkinter.Toplevel(self.parent)
		record_programming_window = Ui_RecordPlanningWindow(top_level, channel_name, channel_url)
		top_level.wait_window(top_level)

	# Open a web site on the corresponding channel TV guide.
	def openWebTVGuide(self, channel_name):
		channel_number = ""
		myPattern = re.compile('^(\d+)')
		m = re.search(myPattern, channel_name)
		try:
			channel_number = m.groups(1)[0]
		except:
			pass
		QtGui.QDesktopServices.openUrl(QtCore.QUrl("http://streamplayer.free.fr/tv/guidetv.php?chaines=freebox-tv#c" + channel_number))

	# Add a channel in the favorite channels list.
	def addToFavorites(self, channel_name):
		global g_favorite_channel_name_list
		g_favorite_channel_name_list.append(channel_name)

		self.list_box_page_2.delete(0, Tkinter.END)
		for channel_name in g_favorite_channel_name_list:
			self.list_box_page_2.insert(Tkinter.END, channel_name)

		self.updateFavoritesFile()

	# Remove a channel from the favorite channels list.
	def removeFromFavorites(self, channel_name):
		global g_favorite_channel_name_list
		g_favorite_channel_name_list.remove(channel_name)

		self.list_box_page_2.delete(0, Tkinter.END)
		for channel_name in g_favorite_channel_name_list:
			self.list_box_page_2.insert(Tkinter.END, channel_name)

		self.updateFavoritesFile()

	# Update the favorite channels list file.
	def updateFavoritesFile(self):
		try:
			favorite_channels_file = open(g_favorites_file_path,'w')
			for channel_name in g_favorite_channel_name_list:
				favorite_channels_file.write(channel_name.encode('utf-8') + "\n")
			favorite_channels_file.close()
		except IOError as msg:
			print("ERROR: Cannot write in " + g_favorites_file_path)
			print(msg)
			sys.exit(1)

	# Display a context menu. This one is called by a right click on a channel name in the main channels list.
	def displayContextMenuAll(self, event):
		# Get the selected channel
		widget = event.widget
		selection_index = widget.nearest(event.y)
		channel_name = self.list_box_page_1.get(selection_index)

		# Build the context menu
		context_menu = Tkinter.Menu(self.parent, tearoff=0)
		context_menu.add_command(label=channel_name, command=self.parent.quit)
		context_menu.add_separator()
		context_menu.add_command(label="Regarder",                            command=lambda: self.watchTV(channel_name))
		context_menu.add_command(label="Enregistrer et regarder",             command=lambda: self.watchAndRecordTV(channel_name))
		context_menu.add_command(label="Planifier un enregistrement",         command=lambda: self.planRecord(channel_name))
		context_menu.add_command(label="Ouvrir le guide TV sur cette chaine", command=lambda: self.openWebTVGuide(channel_name))
		context_menu.add_command(label="Ajouter aux favoris",                 command=lambda: self.addToFavorites(channel_name))
		context_menu.tk_popup(event.x_root, event.y_root)

	# Display a context menu. This one is called by a right click on a channel name in the favorite channels list.
	def displayContextMenuFavorites(self, event):
		# Get the selected channel
		widget = event.widget
		selection_index = widget.nearest(event.y)
		channel_name = self.list_box_page_2.get(selection_index)

		# Build the context menu
		context_menu = Tkinter.Menu(self.parent, tearoff=0)
		context_menu.add_command(label=channel_name, command=self.parent.quit)
		context_menu.add_separator()
		context_menu.add_command(label="Regarder",                            command=lambda: self.watchTV(channel_name))
		context_menu.add_command(label="Enregistrer et regarder",             command=lambda: self.watchAndRecordTV(channel_name))
		context_menu.add_command(label="Planifier un enregistrement",         command=lambda: self.planRecord(channel_name))
		context_menu.add_command(label="Ouvrir le guide TV sur cette chaine", command=lambda: self.openWebTVGuide(channel_name))
		context_menu.add_command(label="Retirer des favoris",                 command=lambda: self.removeFromFavorites(channel_name))
		context_menu.tk_popup(event.x_root, event.y_root)

	# Read configuration file
	def readConfigurationFile(self):
		global g_playlist_url
		try:
			configuration_file = open(g_playlist_url_file_path,'r')
			g_playlist_url = configuration_file.readline()
			configuration_file.close()
		except:
			pass

	# Load the remote Freebox playlist file
	def loadPlaylist(self):
		global g_channel_name_list
		global g_channel_urls_hash_table
		g_channel_name_list = []
		g_channel_urls_hash_table = {}

		playlist_file_lines = []
		try:
			playlist_file = urllib2.urlopen(g_playlist_url)
			playlist_file_lines = playlist_file.readlines()
			playlist_file.close()
		except urllib2.URLError as msg:
			print("WARNING: Cannot open playlist " + g_playlist_url)
			print(msg)
			tkMessageBox.showwarning('Attention', 'La playlist suivante n\'est pas accessible : \n' + g_playlist_url)

		channel_name = ""
		for line in playlist_file_lines:
			if line.startswith('#EXTINF:'):
				splitted_line = string.split(line,",")
				channel_name = splitted_line[1].rstrip()
				channel_name = channel_name.decode('utf-8')
				g_channel_name_list.append(channel_name)
			if line.startswith('rtsp:'):
				channel_url = line.rstrip()
				g_channel_urls_hash_table[channel_name] = channel_url

		self.updateModel()

	# Reload the favorites file
	def reloadFavoritesFile(self):
		global g_favorite_channel_name_list
		if os.path.exists(g_favorites_file_path):
			# Get favorite channels list from the file.
			g_favorite_channel_name_list = []
			file_lines = []
			try:
				favorite_channels_file = open(g_favorites_file_path, 'r')
				file_lines = favorite_channels_file.readlines()
				favorite_channels_file.close()
			except IOError as msg:
				print("ERROR: Cannot read " + g_favorites_file_path)
				print(msg)
				sys.exit(1)

			# Update the favorite channels list and display it.
			self.list_box_page_2.delete(0, Tkinter.END)
			for line in file_lines:
				line = line.rstrip()
				channel_name = line.decode('utf-8')
				g_favorite_channel_name_list.append(channel_name)
				self.list_box_page_2.insert(Tkinter.END, channel_name)

	# Open a window to select a video record from the hard disk.
	def openLocalVideo(self):
		local_video_file_path = tkFileDialog.askopenfilename(initialdir=os.environ['HOME'], title="Charger une video", filetypes = [("Fichier video","*.ts"), ("Tous","*")])
		local_video_file_path = local_video_file_path.encode('utf-8')
		if local_video_file_path != "":
			command = g_vlc + " --video-filter=deinterlace --deinterlace-mode=yadif2x \"" + local_video_file_path + "\" &"
			print(command)
			os.system(command)

	# Open a window which displays information about the script.
	def displayAboutAppWindow(self):
		tkMessageBox.showinfo("A propos", self.parent.title() + "\nRealisation de Jean-Marc VARENNE-PAQUET\nVersion 1.0\nSous licence GNU GPL v3")

	# Display a window to set the playlist to use.
	def displayConfigurationWindow(self):
		top_level = Tkinter.Toplevel(self.parent)
		configuration_window = Ui_ConfigurationWindow(top_level)
		top_level.wait_window(top_level)
		self.loadPlaylist()






# The window to set the playlist to use.
class Ui_ConfigurationWindow():

	# Constructor
	def __init__(self, parent):
		self.parent = parent

		parent.title("Definir une playlist a utiliser")
		parent.geometry("450x100")

		# Entry field for the playlist path.
		self.entry_playlist = ttk.Entry(parent)
		self.entry_playlist.insert(0, g_playlist_url)
		self.entry_playlist.pack(expand="yes", fill="x")

		# Ok, cancel buttons
		buttons_frame = ttk.Frame(parent)
		ok_button = ttk.Button(buttons_frame, text="OK", width=10, command=self.updateConfigurationFile, default=Tkinter.ACTIVE)
		ok_button.pack(side=Tkinter.LEFT, padx=5, pady=5)
		cancel_button = ttk.Button(buttons_frame, text="Cancel", width=10, command=self.parent.destroy)
		cancel_button.pack(side=Tkinter.RIGHT, padx=5, pady=5)
		buttons_frame.pack()

	# Update the configuration file with the new playlist.
	def updateConfigurationFile(self):
		global g_playlist_url
		g_playlist_url = str(self.entry_playlist.get())
		try:
			configuration_file = open(g_playlist_url_file_path,'w')
			configuration_file.write(g_playlist_url)
			configuration_file.close()
		except IOError as msg:
			print("WARNING: Cannot write configuration file " + g_playlist_url_file_path)
			print(msg)
		self.parent.destroy()










# The window to plan video records.
class Ui_RecordPlanningWindow():

	# Constructor
	def __init__(self, parent, channel_name="", channel_url=""):
		self.parent = parent
		self.channel_name = channel_name
		self.channel_url = channel_url
		date_time_format = "%Y-%m-%d %H:%M"
		parent.title("Enregistrement")


		# Enable automatic resizing
		parent.grid_rowconfigure   (0, weight=1)
		parent.grid_columnconfigure(0, weight=1)


		# Display channel name.
		label_channel_name = ttk.Label(parent, text="Enregistrement de " + self.channel_name)
		label_channel_name.grid(column=0, row=0, columnspan=2, sticky='EW')


		# Start date
		label_start = ttk.Label(parent, text="Date de debut")
		label_start.grid(column=0, row=1, sticky='EW')

		self.entry_start = ttk.Entry(parent)
		self.entry_start.insert(0, str(datetime.datetime.now().strftime(date_time_format)))
		self.entry_start.grid(column=1, row=1, sticky='EW')


		# End date
		label_end = ttk.Label(parent, text="Date de fin")
		label_end.grid(column=0, row=2, sticky='EW')

		self.entry_end = ttk.Entry(parent)
		self.entry_end.insert(0, str(datetime.datetime.now().strftime(date_time_format)))
		self.entry_end.grid(column=1, row=2, sticky='EW')


		# Ok, cancel buttons.
		buttons_frame = ttk.Frame(parent)
		ok_button = ttk.Button(buttons_frame, text="OK", width=10, command=self.planRecord, default=Tkinter.ACTIVE)
		ok_button.pack(side=Tkinter.LEFT, padx=5, pady=5)
		cancel_button = ttk.Button(buttons_frame, text="Cancel", width=10, command=self.parent.destroy)
		cancel_button.pack(side=Tkinter.RIGHT, padx=5, pady=5)
		buttons_frame.pack()
		buttons_frame.grid(column=0, row=3, columnspan=2, sticky='EW')

	# Use the contab to plan a record between the two dates.
	def planRecord(self):
		start_date = self.entry_start.get()
		print("Start date: " + start_date)
		end_date = self.entry_end.get()
		print("End date: " + end_date)

		# Convert dates format to fit the crontab format: mm hh jj MMM JJJ task
		tmp_datetime = datetime.datetime.strptime(start_date, "%Y-%m-%d %H:%M")
		start_date_for_cron = tmp_datetime.strftime("%M %H %d %m") + " *"
		print("Start date for crontab: " + start_date_for_cron)
		tmp_datetime = datetime.datetime.strptime(end_date, "%Y-%m-%d %H:%M")
		end_date_for_cron = tmp_datetime.strftime("%M %H %d %m") + " *"
		print("End date for crontab: " + end_date_for_cron)

		# Use the contab to plan a record between the two dates.
		output_file_path = os.environ['HOME'] + os.sep + self.channel_name.replace(" ","") + "__" + start_date.replace(" ", "__").replace(":", "-") + ".ts"
		output_file_path = output_file_path.encode('utf-8')
		print(output_file_path)

		# Write a temporary file with the crontab tasks.
		try:
			tasks_file = open(g_tasks_file,'w')
			tasks_file.write(start_date_for_cron + " " + g_vlc + " \"" + self.channel_url + "\" --intf dummy --sout file/ts:" + output_file_path + "\n")
			tasks_file.write(end_date_for_cron + " killall vlc && crontab -r\n")
			tasks_file.close()
		except IOError as msg:
			print("ERROR: Cannot write in " + g_tasks_file)
			print(msg)
			sys.exit(1)

		# Load the temporary file in crontab.
		command = "crontab " + g_tasks_file
		os.system(command)
		self.parent.destroy()










##########################################################
# Main
##########################################################

if __name__ == "__main__":

	# Display the main window
	root = Tkinter.Tk() # Any tkinter application have only one Tk() instance.
	app = Ui_MainWindow(root)
	root.mainloop()



